package cn.cloud.all.security.oauth2.provider.vote;

import cn.cloud.all.security.access.AccessDecisionVoter;
import cn.cloud.all.security.access.AccessDeniedException;
import cn.cloud.all.security.access.ConfigAttribute;
import cn.cloud.all.security.core.Authentication;
import cn.cloud.all.security.core.AuthorityUtils;
import cn.cloud.all.security.oauth2.common.exceptions.InsufficientScopeException;
import cn.cloud.all.security.oauth2.provider.ClientDetails;
import cn.cloud.all.security.oauth2.provider.ClientDetailsService;
import cn.cloud.all.security.oauth2.provider.OAuth2Authentication;
import cn.cloud.all.security.oauth2.provider.OAuth2Request;

import java.util.Collection;
import java.util.Set;

/**
 * This voter checks scope in request is consistent with that held by the client. If there is no user in the request
 * (client_credentials grant) it checks against authorities of client instead of scopes by default. Activate by adding
 * <code>CLIENT_HAS_SCOPE</code> to security attributes.
 *
 * @author Dave Syer
 */
public class ClientScopeVoter implements AccessDecisionVoter<Object> {

    private String clientHasScope = "CLIENT_HAS_SCOPE";

    private boolean throwException = true;

    private ClientDetailsService clientDetailsService;

    private boolean clientAuthoritiesAreScopes = true;

    /**
     * ClientDetailsService for looking up clients by ID.
     *
     * @param clientDetailsService the client details service (mandatory)
     */
    public void setClientDetailsService(ClientDetailsService clientDetailsService) {
        this.clientDetailsService = clientDetailsService;
    }

    /**
     * Flag to determine the behaviour on access denied. If set then we throw an {@link InsufficientScopeException}
     * instead of returning {@link AccessDecisionVoter#ACCESS_DENIED}. This is unconventional for an access decision
     * voter because it vetos the other voters in the chain, but it enables us to pass a message to the caller with
     * information about the required scope.
     *
     * @param throwException the flag to set (default true)
     */
    public void setThrowException(boolean throwException) {
        this.throwException = throwException;
    }

    /**
     * Flag to signal that when there is no user authentication client authorities are to be treated as scopes.
     *
     * @param clientAuthoritiesAreScopes the flag value (default true)
     */
    public void setClientAuthoritiesAreScopes(boolean clientAuthoritiesAreScopes) {
        this.clientAuthoritiesAreScopes = clientAuthoritiesAreScopes;
    }

    /**
     * The name of the config attribute that can be used to deny access to OAuth2 client. Defaults to
     * <code>DENY_OAUTH</code>.
     *
     * @param denyAccess the deny access attribute value to set
     */
    public void setDenyAccess(String denyAccess) {
        this.clientHasScope = denyAccess;
    }

    public boolean supports(ConfigAttribute attribute) {
		return clientHasScope.equals(attribute.getAttribute());
    }

    /**
     * This implementation supports any type of class, because it does not query the presented secure object.
     *
     * @param clazz the secure object
     * @return always <code>true</code>
     */
    public boolean supports(Class<?> clazz) {
        return true;
    }

    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {

        int result = ACCESS_ABSTAIN;

        if (!(authentication instanceof OAuth2Authentication)) {
            return result;
        }

        OAuth2Authentication oauth2Authentication = (OAuth2Authentication) authentication;
        OAuth2Request clientAuthentication = oauth2Authentication.getOAuth2Request();
        ClientDetails client = clientDetailsService.loadClientByClientId(clientAuthentication.getClientId());
        Set<String> scopes = clientAuthentication.getScope();
        if (oauth2Authentication.isClientOnly() && clientAuthoritiesAreScopes) {
            scopes = AuthorityUtils.authorityListToSet(clientAuthentication.getAuthorities());
        }

        for (ConfigAttribute attribute : attributes) {
            if (this.supports(attribute)) {

                result = ACCESS_GRANTED;

                for (String scope : scopes) {
                    if (!client.getScope().contains(scope)) {
                        result = ACCESS_DENIED;
                        break;
                    }
                }

                if (result == ACCESS_DENIED && throwException) {
                    InsufficientScopeException failure = new InsufficientScopeException("Insufficient scope for this resource", client.getScope());
                    throw new AccessDeniedException(failure.getMessage(), failure);
                }
                return result;
            }
        }
        return result;
    }
}
